/***************************************************************************
 *   oboe.math.Mesh3D                                                      *
 *   Copyright (C) 2011 by Felipe Manga                                    *
 *   lufeboma@gmail.com                                                    *
 *                                                                         *
 *   Permission is hereby granted, free of charge, to any person obtaining *
 *   a copy of this software and associated documentation files (the       *
 *   "Software"), to deal in the Software without restriction, including   *
 *   without limitation the rights to use, copy, modify, merge, publish,   *
 *   distribute, sublicense, and/or sell copies of the Software, and to    *
 *   permit persons to whom the Software is furnished to do so, subject to *
 *   the following conditions:                                             *
 *                                                                         *
 *   The above copyright notice and this permission notice shall be        *
 *   included in all copies or substantial portions of the Software.       *
 *                                                                         *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       *
 *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    *
 *   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. *
 *   IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR     *
 *   OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
 *   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
 *   OTHER DEALINGS IN THE SOFTWARE.                                       *
 ***************************************************************************/

package oboe.math
{
	import flash.utils.Dictionary;
	
	import oboe.controllers.SceneController;
	import oboe.core.Oboe;
	import oboe.core.RenderSettings;

	public class Mesh3D extends Mesh2D
	{
		private var uv:Vector.<Number>;
		private var frames3D:Vector.< Vector.<Number3D> >;
		private var indices:Vector.<int>;
		private static var zscratch:Vector.<Number> = new Vector.<Number>();
		
		private static const tBuffer:Vector.<Triangle3D> = new Vector.<Triangle3D>();
		private static const outTBuffer:Array = [];
		private var cache:Dictionary;

		private static const pa:Number3D = new Number3D(0,0,0);
		private static const pb:Number3D = new Number3D(0,0,0);
		private static const pc:Number3D = new Number3D(0,0,0);
		private static const tmp:Number3D = new Number3D(0,0,0);
		private static const tmpA:Number3D = new Number3D(0,0,0);
		private static const tmpB:Number3D = new Number3D(0,0,0);
		
		private var curFrameTris:Vector.<Number> = new Vector.<Number>();
		
		public function Mesh3D( indices:Vector.<int>, mesh:Vector.< Vector.<Number3D> >, uvs:Vector.<Number> )
		{
			super(null);
			this.uv = uvs;
			this.frames3D = mesh;
			if( !indices && mesh && mesh.length )
			{
				indices = new Vector.<int>( mesh[0].length, true );
				var l:int = indices.length;
				for( var i:int=0; i<l; ++i )
				{
					indices[i] = i;
				}
			}
			this.indices = indices;
			this.cache = new Dictionary();
		}
		
		public function forEachVertex( s:RenderSettings, func:Function ):void
		{
			if( s.frame > frames3D.length ) return;
			if( frames3D[ int(s.frame) ].length <= i ) return;
			var frame:Vector.<Number3D> = frames3D[ int(s.frame) ];
			var l:int = frame.length;
				
			for( var i:int=0; i<l; ++i )
			{
				func( frame[i], i );
			}
		}
		
		public function getIndexFromApproxUV( u:Number, v:Number ):int
		{
			var vc:int=uv.length;
			var pd:Number=0xFFFF;
			var lvp:int=-1;
			var tud:Number;
			var tvd:Number;
			var td:Number;
			u -= int(u);
			v -= int(v);
			for( var i:int=0; i<vc; i+=2 )
			{
				tud = uv[ i ];
				tud -= int(tud) + u;
				tvd = uv[ i+1 ];
				tvd -= int(tvd) + v;
				td = tud*tud+tvd*tvd;
				if( td<pd )
				{
					pd = td;
					lvp = i;
				}
			}
			return lvp>>1;
		}
		
		private static const tempRot:Rotator = new Rotator(0,0,0);
		public function getTransformedVertex( s:RenderSettings, i:int ):Number3D
		{
			if( s.frame > frames3D.length ) return null;
			if( frames3D[ int(s.frame) ].length <= i ) return null;
		
			tempRot.copy3( s.rotation );
			tempRot.update();
			
			tmp.copy3( frames3D[ int(s.frame) ][ i ] );
			tmp.mul3( s.scale );
			tempRot.apply( tmp );
			tmp.add3( s.position );
			return tmp;
		}
		
		public function getVertex( s:RenderSettings, i:int ):Number3D
		{
			if( s.frame > frames3D.length ) return null;
			if( frames3D[ int(s.frame) ].length <= i ) return null;
			tmp.copy3( frames3D[ int(s.frame) ][ i ] );
			return tmp;
		}
		
		public override function draw( s:RenderSettings ):void
		{
			if( s.frame > frames3D.length ) return;
			
			var cacheKey:String = s.getCacheKey();
			this.frames = this.cache[ cacheKey ];
			if( !this.frames )
			{
				if( s.debugCache )
				{
					trace("Cache miss on ", cacheKey);
				}
				this.frames = new Vector.<Frame>(frames3D.length, true);
				if( s.mode&RenderSettings.MODE_CACHE_ONLY )
				{
					this.cache[ cacheKey ] = this.frames;					
				}
			}
			
			if( !this.frames[ int(s.frame) ] )
			{
				while( indices.length > tBuffer.length )
				{
					tBuffer.push( new Triangle3D() );
				}
				outTBuffer.length = tBuffer.length;
				var frame3D:Vector.<Number3D> = frames3D[ int(s.frame) ];
				var verts:Vector.<Number> = new Vector.<Number>( frame3D.length*2, true );
				var uvs:Vector.<Number> = this.uv;
				outTBuffer.length = this.renderFrame( frame3D, verts, null, s );
				var frame2D:Frame = new Frame( outTBuffer, verts, uvs );
				this.frames[ int(s.frame) ] = frame2D;
			}

			switch( s.projection )
			{
				case RenderSettings.PROJECT_PARALLEL:
					s.containerShiftOut.set( s.position.x-SceneController._camera_position.x, -s.position.y+SceneController._camera_position.y );
					break;
				case RenderSettings.PROJECT_ISOMETRIC:
					tmp.copy3( s.position ).sub3( SceneController._camera_position );
					s.containerShiftOut.set( -tmp.x+tmp.z, -tmp.y + (tmp.x+tmp.z)*0.5 );
					break;
				case RenderSettings.PROJECT_PERSPECTIVE:
					s.containerShiftOut.set( 0, 0 );
					break;
			}
			
			s.containerShiftOut.add2( s.containerShiftIn );
			
			super.draw( s );
		}
		
		private function renderFrame( frame:Vector.<Number3D>, verts:Vector.<Number>, uvs:Vector.<Number>, s:RenderSettings ):int
		{
			var tbp:int=0;
			if( !s.rotation.isEqual( s.rotator ) )
			{
				s.rotator.copy3( s.rotation );
				s.rotator.update();
			}
			
			if( SceneController._camera_position )
			{
				RenderSettings.finalPosition.copy3( s.position ).sub3( SceneController._camera_position );
			}
			else
			{
				RenderSettings.finalPosition.copy3( s.position );
			}
			
			var maxScreenX:Number;
			var maxScreenY:Number;
			var screenCulling:Boolean = false;
			var transform:Function;
			
			if( s.projection == RenderSettings.PROJECT_PERSPECTIVE )
			{
				maxScreenX = Oboe.stage.stageWidth/2;
				maxScreenY = Oboe.stage.stageHeight/2;
				screenCulling = true;
				transform = this.transformPerspective;
			}
			else if( s.projection == RenderSettings.PROJECT_ISOMETRIC )
			{
				transform = this.transformIsometric;
			}
			else
			{
				transform = this.transformParallel;
			}

			var cv:int;
			var iid:int;
			var length:int;
			iid=0;
			
			if( zscratch.length < frame.length )
			{
				zscratch.length = frame.length;
			}
			transform( frame, verts, s );
			
			var tri:Triangle3D;
			length = indices.length;
			switch( s.culling )
			{
			case RenderSettings.CULL_NONE:
				for( iid=0; iid<length; iid+=3 )
				{
					pa.z = zscratch[ indices[iid  ] ];
					pb.z = zscratch[ indices[iid+1] ];
					pc.z = zscratch[ indices[iid+2] ];
					tri = tBuffer[tbp];
					tri.pointA = indices[iid];
					tri.pointB = indices[iid+1];
					tri.pointC = indices[iid+2];
					tri.z = pa.z+pb.z+pc.z;
					outTBuffer[tbp++] = tri;
				}
				break;
			case RenderSettings.CULL_BACK:
				for( iid=0; iid<length; iid+=3 )
				{
					cv = indices[iid  ]; pa.z = zscratch[ cv ]; cv <<= 1; pa.x = verts[ cv++ ]; pa.y = verts[ cv ];
					cv = indices[iid+1]; pb.z = zscratch[ cv ]; cv <<= 1; pb.x = verts[ cv++ ]; pb.y = verts[ cv ];
					cv = indices[iid+2]; pc.z = zscratch[ cv ]; cv <<= 1; pc.x = verts[ cv++ ]; pc.y = verts[ cv ];
					
					tmpA.x = pa.x - pb.x;
					tmpA.y = pa.y - pb.y;
					tmpB.x = pa.x - pc.x;
					tmpB.y = pa.y - pc.y;
					tmp.z = tmpA.x*tmpB.y-tmpA.y*tmpB.x;
					
					if( tmp.z < 0 )
					{
						tri = tBuffer[tbp];
						tri.pointA = indices[iid];
						tri.pointB = indices[iid+1];
						tri.pointC = indices[iid+2];
						tri.z = pa.z+pb.z+pc.z;
						outTBuffer[tbp++] = tri;
					}
				}
				break;
			case RenderSettings.CULL_FRONT:
				for( iid=0; iid<length; iid+=3 )
				{
					cv = indices[iid  ]; pa.z = zscratch[ cv ]; cv <<= 1; pa.x = verts[ cv++ ]; pa.y = verts[ cv ];
					cv = indices[iid+1]; pb.z = zscratch[ cv ]; cv <<= 1; pb.x = verts[ cv++ ]; pb.y = verts[ cv ];
					cv = indices[iid+2]; pc.z = zscratch[ cv ]; cv <<= 1; pc.x = verts[ cv++ ]; pc.y = verts[ cv ];
					tmpA.x = pa.x - pb.x;
					tmpA.y = pa.y - pb.y;
					tmpB.x = pa.x - pc.x;
					tmpB.y = pa.y - pc.y;
					tmp.z = tmpA.x*tmpB.y-tmpA.y*tmpB.x;
					
					if( tmp.z > 0 )
					{
						tri = tBuffer[tbp];
						tri.pointA = indices[iid];
						tri.pointB = indices[iid+1];
						tri.pointC = indices[iid+2];
						tri.z = pa.z+pb.z+pc.z;
						outTBuffer[tbp++] = tri;
					}
				}
				break;
			}
			return tbp;
		}
		
		private function transformParallel( frame:Vector.<Number3D>, verts:Vector.<Number>, s:RenderSettings ):void // vert:Number3D, out:Number3D, s:RenderSettings ):void
		{
			var iid:int;
			var length:int;
			var xvx : Number = s.rotator.xvx;
			var xvy : Number = s.rotator.xvy;
			var xvz : Number = s.rotator.xvz;
			var yvx : Number = s.rotator.yvx;
			var yvy : Number = s.rotator.yvy;
			var yvz : Number = s.rotator.yvz;
			var zvx : Number = s.rotator.zvx;
			var zvy : Number = s.rotator.zvy;
			var zvz : Number = s.rotator.zvz;
			var vert:Number3D;
			var z:Number;
			var cv:int;
			length=frame.length;
			iid=0;
			for( cv=0; cv<length; cv++ )
			{
				vert = frame[cv];
				// s.rotator.constapply( vert, tmp );
				tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
				tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
				tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
				
				tmp.x = tmp.x * s.scale.x;
				tmp.y = tmp.y * s.scale.y;
				tmp.z = tmp.z * s.scale.z;
				
				verts[ iid++ ] = tmp.x;
				verts[ iid++ ] =  -tmp.y;
				zscratch[cv] =  tmp.z;
			}
			
		}
		
		private function transformIsometric( frame:Vector.<Number3D>, verts:Vector.<Number>, s:RenderSettings ):void // vert:Number3D, out:Number3D, s:RenderSettings ):void
		{
			var iid:int;
			var length:int;
			var xvx : Number = s.rotator.xvx;
			var xvy : Number = s.rotator.xvy;
			var xvz : Number = s.rotator.xvz;
			var yvx : Number = s.rotator.yvx;
			var yvy : Number = s.rotator.yvy;
			var yvz : Number = s.rotator.yvz;
			var zvx : Number = s.rotator.zvx;
			var zvy : Number = s.rotator.zvy;
			var zvz : Number = s.rotator.zvz;
			var vert:Number3D;
			var z:Number;
			var cv:int;
			length=frame.length;
			iid=0;
			for( cv=0; cv<length; cv++ )
			{
				vert = frame[cv];
				// s.rotator.constapply( vert, tmp );
				tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
				tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
				tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
				
				tmp.x = tmp.x * s.scale.x;
				tmp.y = tmp.y * s.scale.y;
				tmp.z = tmp.z * s.scale.z;
				
				verts[ iid++ ] = tmp.x - tmp.z;
				verts[ iid++ ] =  -tmp.y + (tmp.x+tmp.z)*0.5;
				zscratch[cv] =  - tmp.z - tmp.x;
			}
		}
		
		private function transformPerspective( frame:Vector.<Number3D>, verts:Vector.<Number>, s:RenderSettings ):void // vert:Number3D, out:Number3D, s:RenderSettings ):void
		{
			var iid:int;
			var length:int;
			var xvx : Number = s.rotator.xvx;
			var xvy : Number = s.rotator.xvy;
			var xvz : Number = s.rotator.xvz;
			var yvx : Number = s.rotator.yvx;
			var yvy : Number = s.rotator.yvy;
			var yvz : Number = s.rotator.yvz;
			var zvx : Number = s.rotator.zvx;
			var zvy : Number = s.rotator.zvy;
			var zvz : Number = s.rotator.zvz;
			var vert:Number3D;
			var z:Number;
			var cv:int;
			length=frame.length;
			iid=0;
			if( SceneController._camera_rotator )
			{
				for( cv=0; cv<length; cv++ )
				{
					vert = frame[cv];
					// s.rotator.constapply( vert, tmp );
					tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
					tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
					tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
					
					tmp.x = tmp.x * s.scale.x + RenderSettings.finalPosition.x;
					tmp.y = tmp.y * s.scale.y + RenderSettings.finalPosition.y;
					tmp.z = tmp.z * s.scale.z + RenderSettings.finalPosition.z;

					SceneController._camera_rotator.apply( tmp );
					
					z = s.focalLength / (s.focalLength + tmp.z);
					verts[ iid++ ] = tmp.x * z;
					verts[ iid++ ] = -tmp.y * z;
					zscratch[cv] = tmp.z;
				}
			}
			else
			{
				for( cv=0; cv<length; cv++ )
				{
					vert = frame[cv];
					// s.rotator.constapply( vert, tmp );
					tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
					tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
					tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
					
					tmp.x = tmp.x * s.scale.x + RenderSettings.finalPosition.x;
					tmp.y = tmp.y * s.scale.y + RenderSettings.finalPosition.y;
					tmp.z = tmp.z * s.scale.z + RenderSettings.finalPosition.z;
					
					z = s.focalLength / (s.focalLength + tmp.z);
					verts[ iid++ ] = tmp.x * z;
					verts[ iid++ ] = -tmp.y * z;
					zscratch[cv] = tmp.z;
				}
			}
		}
	}
}